/**
 * XmlVisitor.cs
 * 
 * AUTHOR:
 * Zlatko Michailov (http://blogs.msdn.com/esql)
 * 
 * PURPOSE:
 * A concrete implementation of the IMultiReaderVisitor interface that 
 * renders results to an XML stream.
 * 
 * COPYRIGHT:
 * (c) Microsoft Corp., 2006-2007
 */



using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Data;
using System.Data.Common;
using System.Data.EntityClient;
using System.Data.Metadata;
using System.Data.Metadata.Edm;
using System.Xml;
using System.Xml.Xsl;
using System.Xml.XPath;



namespace Microsoft.Samples.Data.eSqlBlast
{
    public class XmlVisitor : IMultiReaderVisitor
    {
        // Constants
        private const string _Batch = "Batch";
        private const string _Command = "Command";
        private const string _EntityStatement = "EntityStatement";
        private const string _StoreStatement = "StoreStatement";
        private const string _Exception = "Exception";
        private const string _Collection = "Collection";
        private const string _Record = "Record";
        private const string _Entity = "Entity";
        private const string _Struct = "Struct";
        private const string _Ref = "Ref";
        private const string _Row = "Row";
        private const string _EntitySet = "EntitySet";
        private const string _Key = "Key";
        private const string _Name = "Name";
        private const string _Value = "Value";
        private const string _Null = "Null";
        

        // Data members
        private TextWriter _Output;
        private string _Stylesheet;
        private bool _DumpException;
        private XmlDocument _XmlDoc;
        private XmlElement _XmlBatch;
        private XmlElement _XmlCommand;


        // Keep an explicit default constructor
        public XmlVisitor()
        {
        }


        // Constructors
        public XmlVisitor(TextWriter output, string stylesheet, bool dumpException)
        {
            _Output = output;
            _Stylesheet = stylesheet;
            _DumpException = dumpException;
        }


        // PUBLIC METHODS
        public void BeginBatch()
        {
            _XmlDoc = new XmlDocument();

            if (_Stylesheet != null)
            {
                XmlProcessingInstruction transform = _XmlDoc.CreateProcessingInstruction("xml-stylesheet", string.Format("type='text/xsl' href='{0}'", _Stylesheet));
                _XmlDoc.AppendChild(transform);
            }
            
            _XmlBatch = _XmlDoc.CreateElement(_Batch);
            _XmlDoc.AppendChild(_XmlBatch);
        }


        public void VisitCommand(EntityCommand command)
        {
            _XmlCommand = _XmlDoc.CreateElement(_Command);
            _XmlBatch.AppendChild(_XmlCommand);

            XmlElement xmlEntityStatement = _XmlDoc.CreateElement(_EntityStatement);
            _XmlCommand.AppendChild(xmlEntityStatement);
            xmlEntityStatement.InnerText = command.CommandText;

            XmlElement xmlStoreStatement = _XmlDoc.CreateElement(_StoreStatement);
            _XmlCommand.AppendChild(xmlStoreStatement);
            xmlStoreStatement.InnerText = GetStoreCommandText(command);
        }


        public void VisitException(Exception ex)
        {
            XmlElement xmlException = _XmlDoc.CreateElement(_Exception);
            _XmlCommand.AppendChild(xmlException);
            xmlException.InnerText = _DumpException ? ex.ToString() : ex.Message;
        }


        public void VisitReader(EntityDataReader reader)
        {
            VisitReader(reader, _XmlCommand);
        }


        public void EndBatch()
        {
            _XmlDoc.Save(_Output);
        }


        // PRIVATE METHODS
        private void VisitReader(DbDataReader reader, XmlElement xmlParent)
        {
            XmlElement xmlResult = _XmlDoc.CreateElement(_Collection);
            xmlParent.AppendChild(xmlResult);

            while (reader.Read())
            {
                VisitRecord(reader as IExtendedDataRecord, _Record, xmlResult);
            }
        }


        private void VisitRecord(IExtendedDataRecord record, string elementType, XmlElement xmlParent)
        {
            XmlElement xmlRecord = _XmlDoc.CreateElement(elementType);
            xmlParent.AppendChild(xmlRecord);

            // First get the type kind of the record itself.
            // A record is not necessarily a strucutral type. 
            BuiltInTypeKind recordTypeKind = record.DataRecordInfo.RecordType.EdmType.BuiltInTypeKind;
            int fieldCount;

            // For RefType, PrimitiveType, and CollectionType the FieldMetadata collection is not populated.
            // In those cases the record contains exactly one field.
            if (recordTypeKind == BuiltInTypeKind.RefType ||
                recordTypeKind == BuiltInTypeKind.PrimitiveType ||
                recordTypeKind == BuiltInTypeKind.CollectionType)
            {
                fieldCount = 1;
            }
            else
            {
                fieldCount = record.DataRecordInfo.FieldMetadata.Count;
            }

            for (int i = 0; i < fieldCount; i++)
            {
                string name = record.GetName(i);
                XmlElement xmlProperty = _XmlDoc.CreateElement(XmlConvert.EncodeName(name));
                xmlRecord.AppendChild(xmlProperty);

                // If the field is flagged as DbNull, the shape of the value is undetermined.
                // An attempt to get such a value may trigger an exception.
                if (record.IsDBNull(i))
                {
                    XmlAttribute xmlNull = _XmlDoc.CreateAttribute(_Null);
                    xmlProperty.Attributes.Append(xmlNull);
                    xmlNull.InnerText = true.ToString();
                }
                else
                {
                    BuiltInTypeKind fieldTypeKind = recordTypeKind;
 
                    // If the record is a structural type, examine the field with the given index.
                    if (recordTypeKind == BuiltInTypeKind.EntityType ||
                        recordTypeKind == BuiltInTypeKind.ComplexType ||
                        recordTypeKind == BuiltInTypeKind.RowType)
                    {
                        fieldTypeKind = record.DataRecordInfo.FieldMetadata[i].FieldType.TypeUsage.EdmType.BuiltInTypeKind;
                    }

                    switch (fieldTypeKind)
                    {
                        case BuiltInTypeKind.CollectionType:
                            // Collections are surfaced as nested DbDataReaders.
                            VisitReader(record.GetData(i) as DbDataReader, xmlProperty);
                            break;

                        case BuiltInTypeKind.EntityType:
                            // Entity types are wrapped in implementations of IExtendedDataRecord.
                            VisitRecord(record.GetDataRecord(i) as IExtendedDataRecord, _Entity, xmlProperty);
                            break;

                        case BuiltInTypeKind.ComplexType:
                            // Complex types are wrapped in implementations of IExtendedDataRecord.
                            VisitRecord(record.GetDataRecord(i) as IExtendedDataRecord, _Struct, xmlProperty);
                            break;

                        case BuiltInTypeKind.RowType:
                            // Row types are wrapped in implementations of IExtendedDataRecord.
                            VisitRecord(record.GetDataRecord(i) as IExtendedDataRecord, _Row, xmlProperty);
                            break;

                        case BuiltInTypeKind.RefType:
                            // Ref types are surfaced as EntityKey instances. The containing record sees them as atomic.
                            VisitKey(record.GetValue(i) as EntityKey, xmlProperty);
                            break;

                        case BuiltInTypeKind.PrimitiveType:
                            // Primitive types are surfaced as plain objects.
                            xmlProperty.InnerText = record.GetValue(i).ToString();
                            break;

                        default:
                            // Unhandled type kind. We shouldn't end up here.
                            xmlProperty.InnerText = "Unhandled type kind [" + fieldTypeKind.ToString() + "]";
                            break;
                    }
                }
            }
        }


        private void VisitKey(EntityKey key, XmlElement xmlParent)
        {
            XmlElement xmlRef = _XmlDoc.CreateElement(_Ref);
            xmlParent.AppendChild(xmlRef);

            XmlAttribute xmlEntitySet = _XmlDoc.CreateAttribute(_EntitySet);
            xmlRef.Attributes.Append(xmlEntitySet);
            xmlEntitySet.InnerText = key.EntitySetName;

            foreach (EntityKeyMember keyMember in key.EntityKeyValues)
            {
                XmlElement xmlKeyMember = _XmlDoc.CreateElement(_Key);
                xmlRef.AppendChild(xmlKeyMember);

                XmlAttribute xmlName = _XmlDoc.CreateAttribute(_Name);
                xmlKeyMember.Attributes.Append(xmlName);
                xmlName.InnerText = keyMember.Key;

                XmlAttribute xmlValue = _XmlDoc.CreateAttribute(_Value);
                xmlKeyMember.Attributes.Append(xmlValue);
                xmlValue.InnerText = keyMember.Value.ToString();
            }
        }


        private string GetStoreCommandText(EntityCommand command)
        {
            command.Prepare();
            return command.ToTraceString();
        }

    }

}
